package gui;

import java.awt.AWTException;
import java.awt.Robot;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Scale;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.wb.swt.SWTResourceManager;

import particle_simulator_chem.Boid;
import particle_simulator_chem.World;
import renderer.Camera;

import auxiliary_classes.Notifier;
import auxiliary_classes.Vector4;

/**
 * @author Lane Aasen 
 * @author Eamon Gaffney
 * @author Michael Rosenberger
 * 
 * In MVC configuration, this is the View component.
 * 
 * Goals for rewrite:
 * 	TODO: Implement MVC with extensive threading
 */

public class MainGUI {
	
	private Shell shlMain;
	private Display display;
	
	//TODO make camera a parameter
	private Vector4 los = new Vector4(1, 1, 10);
	private Vector4 loc = new Vector4(1, 1, 1);
	private Camera camera = new Camera(los, loc);
	
	private Robot robocop; //robocop because in the future he will uphold the checksums and protect the data files
	
	private boolean errorReporting;
	private Notifier bugNotifier;
	
	//GUI Start Options
	//minimum window size: (124, 36)
	//recommended default window dimensions: (764, 498)
	private int defaultWindowWidth;
	private int defaultWindowHeight;
	
	private boolean paused;
	private double targetFPS;
	private double sleepCounter; //the static sleep method in Thread takes a long, and since a millisecond sleep each frame is unlikely, smaller amounts of time must be added from multiple frames before sleeping.
	private int frames; //frames since lblFPS was updated
	
	private boolean mouseDown; //used with mouse motion to track the cursor
	private int mouseButton; //used to implement dragging, since MouseMove does not have a button. To circumvent this, when a MouseDown event is triggered, the button is saved to this variable which is then used to determine the appropriate action when a MouseMove event is triggered
	private double mouseX;
	private double mouseY;
	
	private int width;
	private int height;
	private int depth;
	
	private HashMap<String, Integer> keyActions = new HashMap<String, Integer>();
	
	private boolean encircleCursor;
	private int selectionSize;

	private World world;
	
	/**
	 * Constructs a MainGUI using the provided arguments
	 */
	public MainGUI(Shell shell, String title,
			int dimension, 
			String[] recipients, Notifier bugNotifier,
			int configuration, int defaultWindowWidth, int defaultWindowHeight, 
			int width, int height, int depth,
			double targetFPS, int fpsUpdateRate, int fpsResetRate,
			boolean encircleCursor,
			World world) throws FileNotFoundException {
		
		this.shlMain = shell;
		this.shlMain.setText(title);
		
		this.selectionSize = 0;
		
		this.paused = false;
		this.sleepCounter = 0;
	
		this.setBugNotifier(bugNotifier);
		
		this.defaultWindowWidth = defaultWindowWidth;
		this.defaultWindowHeight = defaultWindowHeight;
		
		this.width = width;
		this.height = height;
		this.depth = depth;
		
		this.targetFPS = targetFPS;
		
		this.encircleCursor = encircleCursor;
		
		try {
			this.robocop = new Robot();
		} catch (AWTException awte) {
			awte.printStackTrace();
		}
		
		//this.mainShell.setChildren(children);
		this.world = world;
	}

	/**
	 * Create the window.
	 */
	public void create() {
		this.display = Display.getDefault();
		createContents();
	}
	
	/**
	 * Open the window.
	 */
	public void open() {
		shlMain.open();
		while (!shlMain.isDisposed()) {
			if (!display.readAndDispatch()) {
				display.sleep();
			}
		}
	}

	/**
	 * Create contents of the window.
	 * @wbp.parser.entryPoint
	 */
	protected void createContents() {
		//final Shell shlMain = new Shell(); //comment before running
		shlMain.setSize(800, 500);
		GUIAux.centerWindow(shlMain);
		Boid boid = new Boid(0, 0, 5, 0.0, 0.0, 10.0, new Color(null, 255, 0, 0), "H", "H", world.getElements());
		world.addBoid(boid);
		
		Menu controlMenu = new Menu(shlMain, SWT.BAR);
		shlMain.setMenuBar(controlMenu);
		
		MenuItem mntmTools = new MenuItem(controlMenu, SWT.CASCADE);
		mntmTools.setText("Tools");
		
		Menu menuTools = new Menu(mntmTools);
		mntmTools.setMenu(menuTools);
		
		MenuItem mntmPopulationControl = new MenuItem(menuTools, SWT.NONE);
		mntmPopulationControl.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				world.setCurrentTool(0); //sets current tool to population controller
			}
		});
		mntmPopulationControl.setAccelerator(SWT.MOD1 + 'A');
		mntmPopulationControl.setText("Population Control\tCtrl + A");
		
		MenuItem mntmSelectBoid = new MenuItem(menuTools, SWT.NONE);
		mntmSelectBoid.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				SearchableBoidSelect boidSelect = new SearchableBoidSelect(shlMain, shlMain.getStyle(), world, "Boid Select", 0, 0);
				boidSelect.open();
			}
		});
		mntmSelectBoid.setAccelerator(SWT.MOD1 + 'S');
		mntmSelectBoid.setText("Select Boid\tCtrl + S");
		
		MenuItem mntmDestroyAllBoids = new MenuItem(menuTools, SWT.NONE);
		
		mntmDestroyAllBoids.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				world.setBoids(new ArrayList<Boid>(0));
			}
		});
		mntmDestroyAllBoids.setAccelerator(SWT.MOD1 + 'C');
		mntmDestroyAllBoids.setText("Destroy All Boids\tCtrl + C");
		
		MenuItem mntmPause = new MenuItem(menuTools, SWT.NONE);
		mntmPause.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				System.out.println("ctrl + space");
			}
		});
		mntmPause.setAccelerator(SWT.MOD1 + 'Q');
		mntmPause.setText("Pause");
		
		new MenuItem(menuTools, SWT.SEPARATOR);
		
		MenuItem mntmGravity = new MenuItem(menuTools, SWT.CASCADE);
		mntmGravity.setText("Gravity");
		mntmGravity.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				world.resetGravity();
			}
		});
		
		Menu menu_2 = new Menu(mntmGravity);
		mntmGravity.setMenu(menu_2);
		
		MenuItem mntmGravityControl = new MenuItem(menu_2, SWT.NONE);
		mntmGravityControl.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				world.setCurrentTool(2);
			}
		});
		mntmGravityControl.setAccelerator(SWT.MOD1 + 'G');
		mntmGravityControl.setText("Gravity Control\tCtrl + G");
		
		MenuItem mntmResetGravity = new MenuItem(menu_2, SWT.NONE);
		mntmResetGravity.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				world.resetGravity();
			}
		});
		mntmResetGravity.setAccelerator(SWT.MOD1 + 'R');
		mntmResetGravity.setText("Reset Gravity\tCtrl + R");
		
		MenuItem mntmSettings = new MenuItem(controlMenu, SWT.CASCADE);
		mntmSettings.setText("Settings");
		
		Menu menuSettings = new Menu(mntmSettings);
		mntmSettings.setMenu(menuSettings);
		
		MenuItem mntmWorldSettings = new MenuItem(menuSettings, SWT.NONE);
		mntmWorldSettings.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				int windowX = shlMain.getLocation().x + shlMain.getClientArea().width;
				//Different operating systems have different window borders, which must be accounted for to make seamless windows
				//However there are also many different GUI options in the operating system, which cannot be detected in this fashion
				//TODO: Revise window border compensation so that it works regardless of operating system GUI options
				if (System.getProperty("os.name").toLowerCase().contains("xp")){ 
					windowX += 7;
				} else if (System.getProperty("os.name").toLowerCase().contains("7")){ //
					windowX += 16;
				} 
				WorldSettings worldset = new WorldSettings(shlMain, shlMain.getStyle(), world, windowX, shlMain.getLocation().y);
				worldset.open();
			}
		});
		mntmWorldSettings.setAccelerator(SWT.MOD1 + 'W');
		mntmWorldSettings.setText("World Settings\t Ctrl + W");
		
		MenuItem mntmFile = new MenuItem(controlMenu, SWT.CASCADE);
		mntmFile.setText("File");
		
		Menu menuFile = new Menu(mntmFile);
		mntmFile.setMenu(menuFile);
		
		MenuItem mntmSaveWorld = new MenuItem(menuFile, SWT.NONE);
		mntmSaveWorld.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				SaveWorld worldsave = new SaveWorld(shlMain, shlMain.getStyle(), world);
				worldsave.open();
			}
		});
		mntmSaveWorld.setText("Save World");
		
		MenuItem mntmLoadWorld = new MenuItem(menuFile, SWT.NONE);
		mntmLoadWorld.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				LoadWorld worldload = new LoadWorld(shlMain, shlMain.getStyle(), world);
				worldload.open();
			}
		});
		mntmLoadWorld.setText("Load World");
		
		/**
		 * monitorComposite makes up the bottom section of the MainGUI window.
		 * It displays useful and recent information including boid count and FPS, along with a scale for adjusting the power of certain tools.
		 */
		final Composite monitorComposite = new Composite(shlMain, SWT.BORDER);
		monitorComposite.setBounds(10, 408, 764, 26);
		
		final Label lblBoidCount = new Label(monitorComposite, SWT.NONE);
		lblBoidCount.setBounds(10, 0, 192, 22);
		lblBoidCount.setText("Boid Count:\r\n");
		
		final Scale sclSelectionSize = new Scale(monitorComposite, SWT.NONE);
		sclSelectionSize.setSelection(10); 
		sclSelectionSize.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent se) {
				selectionSize = sclSelectionSize.getSelection();
			}
		});
		sclSelectionSize.setToolTipText("Determines the power/area of influence of all tools");
		sclSelectionSize.setBounds(253, -10, 250, 39);
		
		final Label lblFPS = new Label(monitorComposite, SWT.NONE);
		lblFPS.setBounds(519, 0, 241, 22);
		lblFPS.setText("FPS:\r\n");
		
		final Label vsp1 = new Label(monitorComposite, SWT.SEPARATOR | SWT.VERTICAL); //Separates 'Boid Count' and the selectionSize slider
		vsp1.setBounds(245, 0, 2, 22);
		
		final Label vsp2 = new Label(monitorComposite, SWT.SEPARATOR | SWT.VERTICAL); //Separates the selectionSize slider and 'FPS'
		vsp2.setBounds(509, -17, 2, 64);
		
		final Canvas canvas = new Canvas(shlMain, SWT.BORDER | SWT.NO_BACKGROUND);
		canvas.addKeyListener(new KeyAdapter() {
			/**
			 * Processes key presses in canvas based on user settings.
			 */
			@Override
			public void keyPressed(KeyEvent ke) { //keycodes not fit for human consumption
				int keyCode = ke.keyCode;
				System.out.println(keyCode);
				
				keyActions.put("escape", 27);
				keyActions.put("forward", 119);
				
				try{
					if(keyCode == keyActions.get("forward")) {
						System.out.println("escape");
						paused = !paused;

						//mainShell.close();
						//move camera forwards
					} else if(keyCode == keyActions.get("backward")) {
						//move camera left
					} else if(keyCode == keyActions.get("left")) {
						//move camera right
					} else if(keyCode == keyActions.get("right")) {
						//move camera backwards
					} else if(keyCode == keyActions.get("escape")) {
						System.out.println("escape");
						paused = true;
					} else if(keyCode == keyActions.get("inventory")) {

					} 
				} catch(NullPointerException npe) {
					
				}

				System.out.println("Character: " + ke.character);
				System.out.println("Keycode: " + ke.keyCode);
				System.out.println("Key Loc: " + ke.keyLocation + "\n");
			}
		});

		canvas.setFont(SWTResourceManager.getFont("Segoe UI", 9, SWT.ITALIC));
		canvas.setBackground(SWTResourceManager.getColor(SWT.COLOR_WHITE));
		canvas.setCursor(SWTResourceManager.getCursor(SWT.NONE));
		canvas.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseDown(MouseEvent e) {
				
				mouseX = e.x;
				mouseY = e.y;
				//System.out.println(MouseInfo.getPointerInfo().getLocation().getX() + ", " + MouseInfo.getPointerInfo().getLocation().getX());
				//world.addBoid(crystalBoid(MouseInfo.getPointerInfo().getLocation().getX(), MouseInfo.getPointerInfo().getLocation().getX()));
				//System.out.println(current_tool);
				mouseDown = true;
				mouseButton = e.button;
				//System.out.println(e.button);
				if(world.getCurrentTool() == 0){//if 'boid control' is selected
					if(mouseButton == 1){//if left mouse button is selected, which should turn the repulsion gun on
						//System.out.println(scale.getSelection());
						//TODO change z value
						world.removeBoidsInRadius(new Vector4(e.x, e.y, 0), sclSelectionSize.getSelection() * 2);
					}else if(mouseButton == 3){
						world.addBoid(formulaBoid(e.x, e.y, 200, world.getCurrentFormula()));
					}
				}else if(world.getCurrentTool() == 2){//if 'gravity manipulator' is selected
					world.setGravity(Math.pow(sclSelectionSize.getSelection(), 3) / 5000);
					if(e.button == 1){//if left mouse button is selected, which should turn the repulsion gun on
						if(world.getGravity() > 0)
							world.setGravity(-world.getGravity()); //gravity must be negative for repulsion gun to function
						world.setGravityLocation(e.x, e.y, 0);
					}else if(e.button == 3){
						if(world.getGravity() < 0)
							world.setGravity(-world.getGravity()); //gravity must be negative for repulsion gun to function
						world.setGravityLocation(e.x, e.y, 0);
					}
				}
			}
			public void mouseUp(MouseEvent e){
				mouseDown = false;
			}
		});
		//Since the gravity tool should be triggered when the mouse is clicked or dragged, it must be the canvas' MouseMoveListener and it's MouseDownListener
		canvas.addMouseMoveListener(new MouseMoveListener() {
			public void mouseMove(MouseEvent e) {
				//System.out.println("Tool: " + current_tool);
				//System.out.println("Button: " + mouseButton);
				mouseX = e.x;
				mouseY = e.y;
				
				//System.out.println("(" + mouseX + ", " + mouseY + ")");

				if(mouseDown){
					if(world.getCurrentTool() == 2){//if 'gravity manipulator' is selected
						//System.out.println("(" + e.x + ", " + e.y + ")");
						world.setGravity(Math.pow(selectionSize, 3) / 5000);
						world.setGravityLocation(e.x, e.y, -10);
						if(mouseButton == 1){//if left mouse button is selected, which should turn the repulsion gun on
							if(world.getGravity() > 0)
								world.setGravity(-world.getGravity()); //gravity must be positive for attraction gun to function
						}else if(mouseButton == 3){
							if(world.getGravity() < 0)
								world.setGravity(-world.getGravity()); //gravity must be negative for repulsion gun to function
						}
					}
					if(world.getCurrentTool() == 0){//if 'boid control' is selected
						if(mouseButton == 1){//if left mouse button is selected, which should turn the repulsion gun on
							//System.out.println(scale.getSelection());
							//TODO change z value
							world.removeBoidsInRadius(new Vector4(e.x, e.y, 0), sclSelectionSize.getSelection() * 2);
						}else if(mouseButton == 3){
							world.addBoid(formulaBoid(e.x, e.y, 200, world.getCurrentFormula()));
						}
					}
				}
				
				/***************************\
				 *                         *
				 *  __    _    ___    _    *
				 * || / ||_|| ||__) ||_||  *
				 * ||\\ ||_|| ||__) ||_||  *
				 *                         *
				 * 						   *
				\***************************/
				
//				if(!paused) {
//					Rectangle canvasBounds = canvas.getBounds();
//					org.eclipse.swt.graphics.Point canvasLocation = canvas.toDisplay(canvasBounds.x, canvasBounds.y);
//					int canvasCenterX = canvasLocation.x + (canvasBounds.width / 2);
//					int canvasCenterY = canvasLocation.y + (canvasBounds.height / 2);
//					robocop.mouseMove(canvasCenterX, canvasCenterY);
//				}
			}
		});
		canvas.addPaintListener(new PaintListener() {
			/**
			 * Main loop of the program, updates all sections of the MainGUI including BoidCount, FPS, and the Canvas
			 */
			public void paintControl(PaintEvent e) {//essentially the main loop of the program
				
				long frameStart = System.currentTimeMillis();
				
				if (!paused) {
					world.updateAllBoids();
				}
				
				/**
				 * Implementation of double buffering
				 * An image is created that is the exact size of the canvas, which is then drawn on with world.drawAllBoids() (which has been modified to take a Graphics Component instead of a Pain Event with a Graphics Component
				 * This image is then drawn onto the canvas, with the background and all boids.
				 * Thus, the canvas is illustrated only once per frame, which reduces -or eliminates- screen flicker.
				 * 
				 * In the old drawing method, the background was drawn, followed by boids. This caused a small flicker in between the background and boid illustration events.
				 * Double buffering prevents that by illustrating the background and boids at the same time.
				 */
		        Image bufferImage = (Image) canvas.getData("buffer-image");
		        //Display display = Display.getDefault();
		        if (bufferImage == null //if there is no image
		        		|| bufferImage.getBounds().width != canvas.getSize().x //if the image is incorrectly sized, which could result in unnecessary expenditures or not drawing everything
		        		|| bufferImage.getBounds().height != canvas.getSize().y) {
		        	bufferImage = new Image(display, canvas.getSize().x, canvas.getSize().y);
		        	canvas.setData("buffer-image", bufferImage);
		        }
		        
		        GC bufferImageGC = new GC(bufferImage);
		        bufferImageGC.setBackground(e.gc.getBackground());
		        bufferImageGC.setForeground(e.gc.getForeground());
		        
		        Rectangle background = bufferImage.getBounds();
		        bufferImageGC.fillRectangle(0, 0, background.width, background.height);
		        
				world.drawAllBoids(bufferImageGC, MainGUI.this.camera);

		        e.gc.drawImage(bufferImage, 0, 0);       
		        
		        bufferImageGC.dispose();
		        
		        canvas.redraw();
				
				/**
				 * Updates lblBoidCount
				 */
				lblBoidCount.setText("Boid Count: " + world.getNumBoids());
		        	
				/**
				 * Makes the GUI thread sleep
				 * 
				 * Each frame is allocated 1000 / targetFPS milliseconds to execute. 
				 * The thread then sleeps for the remainder of the allocated time.
				 */
				//System.out.println(elapsedTime);
				long framePhysicsEnd = System.currentTimeMillis();
				long elapsedTime = framePhysicsEnd - frameStart;
				//System.out.println("ET: " + elapsedTime);
				double projectedSleep = (((1000.0 / (double) targetFPS) - elapsedTime));
				sleepCounter += projectedSleep;
				//System.out.println("PS: " + projectedSleep);
				//System.out.println("TT: " + ((double) elapsedTime + projectedSleep));

				if (sleepCounter >= 1) {
					try {
						Thread.sleep((long) sleepCounter);
						sleepCounter = sleepCounter % 1.0;
					} catch (InterruptedException ie) {
						ie.printStackTrace();
					}
				}
				if (MainGUI.this.frames % 10 == 0) {
					lblFPS.setText("FPS: " + (int) (1000.0 / (System.currentTimeMillis() - frameStart)));
				}
				frames++;
			}
		});
		canvas.setBounds(10, 10, 760, 392);
		
		/**
		 * Resizes all controls and components of the MainGUI, as well as the world. 
		 * Once 3D rendering is implemented, the render area will be resized instead of world.
		 * 
		 * TODO: Adapt for 3D rendering by not resizing world.
		 */
		shlMain.addControlListener(new ControlAdapter() {
			@Override
			public void controlResized(ControlEvent e) {
				int width = shlMain.getBounds().width - 36;
				int height = shlMain.getBounds().height - shlMain.getMinimumSize().y - 70;
				canvas.setBounds(10, 10, width, height);
				world.resize(width - 10, height - 10);
				monitorComposite.setBounds(10, height + 15, width, 26);
				int componentWidth = (int) ((width - 40) / 3);
				lblBoidCount.setBounds(10, 0, componentWidth, 22);
				vsp1.setBounds(componentWidth + 15, 0, 10, 22);
				sclSelectionSize.setBounds(componentWidth + 20, -10, componentWidth, 42);
				vsp2.setBounds(2 * componentWidth + 20, 0, 10, 22);
				lblFPS.setBounds(2 * componentWidth + 30, 0, componentWidth, 22);
			}
		});
	}

	public Boid formulaBoid(double x, double y, double z, String formula){
		//System.out.println(formula);
		Boid boid = new Boid(x, y, z, 0.0, 0.0, 10.0, new Color(null, 255, 0, 0), formula, formula, this.world.getElements());
		return boid;
	}

	public void setBugNotifier(Notifier bugNotifier) {
		this.bugNotifier = bugNotifier;
	}

	public Notifier getBugNotifier() {
		return bugNotifier;
	}
}